package com.yanh.common.security;

import com.yanh.common.config.JwtConfig;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecureDigestAlgorithm;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class TokenUtil {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private JwtConfig jwtProperties;

    /**
     * 加密算法
     */
    private final static SecureDigestAlgorithm<SecretKey, SecretKey> ALGORITHM = Jwts.SIG.HS256;

    //构建map存储令牌需携带的信息
    private Map<String, Object> buildClaims(String userId, String username, Map<String, String> payloads) {
        int payloadSizes = payloads == null? 0 : payloads.size();

        Map<String, Object> claims = new HashMap<>(payloadSizes + 2);
        claims.put(TokenConstant.USER_ID, userId);
        claims.put(TokenConstant.USER_NAME, username);
        claims.put("created", new Date());
        if(payloadSizes > 0){
            claims.putAll(payloads);
        }
        return claims;
    }

    /**
     * 生成令牌
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis()
                + jwtProperties.getExpiration());

        return Jwts.builder()
                .header()
                .add("typ", "JWT")
                .add("alg", "HS256")
                .and()
                .claims(claims)
                .id(UUID.randomUUID().toString())
                .expiration(expirationDate)
                // 签发时间
                .issuedAt(new Date())
                // 主题
                .subject(jwtProperties.getSubject())
                // 签发者
                .issuer(jwtProperties.getIssue())
                // 签名
                .signWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()), ALGORITHM)
                .compact();
    }

    /**
     * 生成刷新令牌 refreshToken，有效期是令牌的 2 倍
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateRefreshToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + jwtProperties.getExpiration() * 2);

        return Jwts.builder()
                .header()
                .add("typ", "JWT")
                .add("alg", "HS256")
                .and()
                .claims(claims)
                .id(UUID.randomUUID().toString())
                .expiration(expirationDate)
                // 签发时间
                .issuedAt(new Date())
                // 主题
                .subject(jwtProperties.getSubject())
                // 签发者
                .issuer(jwtProperties.getIssue())
                // 签名
                .signWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()), ALGORITHM)
                .compact();
    }

    //生成令牌
    private Map<String, Object> buildToken(String userId, String username) {
        //生成token令牌
        String accessToken = generateToken(userId, username, null);
        //生成刷新令牌
        String refreshToken = generateRefreshToken(userId, username, null);
        //存储两个令牌及过期时间，返回结果
        HashMap<String, Object> tokenMap = new HashMap<>(2);
        tokenMap.put(TokenConstant.ACCESS_TOKEN, accessToken);
        tokenMap.put(TokenConstant.REFRESH_TOKEN, refreshToken);
        tokenMap.put(TokenConstant.EXPIRE_IN, jwtProperties.getExpiration());
        return tokenMap;
    }

    //将token缓存进redis
    private void cacheToken(String userId, Map<String, Object> tokenMap) {
        stringRedisTemplate.opsForHash().put(TokenConstant.JWT_CACHE_KEY + userId, TokenConstant.ACCESS_TOKEN, tokenMap.get(TokenConstant.ACCESS_TOKEN));
        stringRedisTemplate.opsForHash().put(TokenConstant.JWT_CACHE_KEY + userId, TokenConstant.REFRESH_TOKEN, tokenMap.get(TokenConstant.REFRESH_TOKEN));
        stringRedisTemplate.expire(TokenConstant.JWT_CACHE_KEY + userId, jwtProperties.getExpiration() * 2, TimeUnit.MILLISECONDS);
    }

    /**
     * 从令牌中获取数据声明,验证 JWT 签名
     *
     * @param token 令牌
     * @return 数据声明
     */
    public Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims =Jwts.parser()
                    .verifyWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()))
                    .build()
                    .parseSignedClaims(token)
                    .getPayload();

        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 裁剪token前缀
     */
    public  String replaceTokenPrefix(String token) {
        // 如果前端设置了令牌前缀，则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstant.PREFIX)) {
            token = token.replaceFirst(TokenConstant.PREFIX, "");
        }
        return token;
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = (String) claims.get(TokenConstant.USER_NAME);
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 从令牌中获取用户id
     *
     * @param token 令牌
     * @return 用户id
     */
    public String getUserIdFromToken(String token) {
        String userId;
        try {
            Claims claims = getClaimsFromToken(token);
            userId = (String) claims.get(TokenConstant.USER_ID);
        } catch (Exception e) {
            userId = null;
        }
        return userId;
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return true=已过期，false=未过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            //验证 JWT 签名失败等同于令牌过期
            return true;
        }
    }

    /**
     * 判断令牌是否不存在 redis 中
     *
     * @param token 刷新令牌
     * @return true=不存在，false=存在
     */
    public Boolean isRefreshTokenNotExistCache(String token) {
        String userId = getUserIdFromToken(token);
        String refreshToken = (String)stringRedisTemplate.opsForHash().get(TokenConstant.JWT_CACHE_KEY + userId, TokenConstant.REFRESH_TOKEN);
        return refreshToken == null || !refreshToken.equals(token);
    }
    //缓存中删除token
    public boolean removeToken(String userId) {
        return Boolean.TRUE.equals(stringRedisTemplate.delete(TokenConstant.JWT_CACHE_KEY + userId));
    }

    /**
     * 生成 token 令牌 及 refresh token 令牌
     * @param payloads 令牌中携带的附加信息
     * @return 令牌
     */
    public String generateToken(String userId, String username,
                                Map<String,String> payloads) {
        Map<String, Object> claims = buildClaims(userId, username, payloads);;

        return generateToken(claims);
    }
    public String generateRefreshToken(String userId, String username, Map<String,String> payloads) {
        Map<String, Object> claims = buildClaims(userId, username, payloads);

        return generateRefreshToken(claims);
    }

    /**
     * 生成 token 令牌主方法
     * @param userId 用户Id或用户名
     * @return 令token牌
     */

    public Map<String, Object> generateTokenAndRefreshToken(String userId, String username) {
        //生成令牌及刷新令牌
        Map<String, Object> tokenMap = buildToken(userId, username);
        //redis缓存结果
        cacheToken(userId, tokenMap);

        return tokenMap;
    }

    /**
     * 刷新令牌并生成新令牌
     * 并将新结果缓存进redis
     */
    public Map<String, Object> refreshTokenAndGenerateToken(String userId, String username) {
        Map<String, Object> tokenMap = buildToken(userId, username);
        stringRedisTemplate.delete(TokenConstant.JWT_CACHE_KEY + userId);
        cacheToken(userId, tokenMap);
        return tokenMap;
    }
}